package org.xtext.runtime.interpreter;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xtext.runtime.formula.Event;
import org.xtext.runtime.formula.MentalEvent;
import org.xtext.runtime.formula.Predicate;
import org.xtext.runtime.plan.PlanInvocationStep;
import org.xtext.runtime.plan.PlanStack;
import org.xtext.runtime.teleoreactive.TRFunction;
import org.xtext.runtime.util.Bindings;
import org.xtext.runtime.util.Unifier;

public class ASTRAClass {
	private static final String BASE_CLASS = "astra.lang.Agent";
	private String name;
	private String _package;
	private List<String> parents = new LinkedList<String>();
	private LinkedList<MentalEvent> initialEvents = new LinkedList<MentalEvent>();
	private Map<String, List<Rule>> rules = new HashMap<String, List<Rule>>();
	private Map<String, TRFunction> functions = new HashMap<String, TRFunction>();
	private Map<String, List<Plan>> planMap = new HashMap<String, List<Plan>>();
	private LinkedList<Predicate> beliefs = new LinkedList<Predicate>();
	
	public ASTRAClass(String _package, String string) {
		name = string;
		this._package = _package;
	}

	public static ASTRAClass forName(String url) throws ASTRAClassNotFoundException {
		return ASTRAClassLoader.getDefaultClassLoader().loadClass(null, url);
	}
	
	public static ASTRAClass forName(String _package, String url) throws ASTRAClassNotFoundException {
		return ASTRAClassLoader.getDefaultClassLoader().loadClass(_package, url);
	}
	
	public String getName() {
		return name;
	}
	
	public void addParent(String parent) {
		parents.add(parent);
	}
	
	public String getPackage() {
		return _package;
	}
	
	public String getCanonicalName() {
		return _package + "." + name;
	}
	
	public void addEvent(MentalEvent event) {
		initialEvents.add(event);
	}
	
	protected void addRule(Rule rule) {
		List<Rule> list = rules.get(rule.event().signature());
		if (list == null) {
			list = new LinkedList<Rule>();
			rules.put(rule.event().signature(), list);
		}
		
		list.add(rule);
	}
	
	private static Map<String, Agent> agents = new HashMap<String,Agent>();
	
	public static Agent getInstance(String name) {
		return agents.get(name);
	}
	
	public synchronized Agent newInstance(String name) throws DuplicateAgentNameException {
		if (agents.containsKey(name)) {
			throw new DuplicateAgentNameException("An agent with name: \"" + name + "\" already exists.");
		}
		
		Agent agent = new Agent(name);
		agent.setLinearization(getLinearization());
		agents.put(name, agent);
		return agent;
	}
	
	public List<ASTRAClass> getLinearization() {
		return getLinearization(new LinkedList<ASTRAClass>());
	}
	
	private List<ASTRAClass> getLinearization(LinkedList<ASTRAClass> linearization) {
		linearization.add(this);
		for (String parent : parents) {
			try {
				ASTRAClass.forName(_package, parent).getLinearization(linearization);
			} catch (ASTRAClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		return linearization;
	}

	public Set<String> getEventFilter() {
		Set<String> filter = new HashSet<String>();
		for (List<Rule> list : rules.values()) {
			for (Rule rule : list) {
				String sig = rule.event().signature();
				if (!filter.contains(sig)) {
					filter.add(sig);
				}
			}
		}
		
		List<ASTRAClass> classes = getParentClasses();
		if (!classes.isEmpty()) {
			for (ASTRAClass clazz : classes) {
				filter.addAll(clazz.getEventFilter());
			}
		}
		
//		System.out.println("[" + this.name + "] filter: " + filter);
		return filter;
	}

	public LinkedList<MentalEvent> getInitialEvents() {
		return initialEvents;
	}
	
	private List<ASTRAClass> getParentClasses() {
		List<ASTRAClass> classes = new LinkedList<ASTRAClass>();
		
		if (!getCanonicalName().equals(ASTRAClass.BASE_CLASS)) {
			try {
				if (parents == null) { 
					classes.add(ASTRAClass.forName(ASTRAClass.BASE_CLASS));
				} else {
					for (String parent : parents) {
						classes.add(ASTRAClass.forName(_package, parent));
					}
				}
			} catch (ASTRAClassNotFoundException e) {
				e.printStackTrace();
				return null;
			}
		}

		return classes;
	}

	/**
	 * Add a new plan to the class
	 * 
	 * @param plan
	 */
	public void addPlan(Plan plan) {
//		System.out.println("Adding plan: " +plan);
		List<Plan> list = planMap.get(plan.getLabel().identifier());
		if (list == null) {
			list = new LinkedList<Plan>();
			planMap.put(plan.getLabel().identifier(), list);
		}

		list.add(plan);
	}
	
	/**
	 * Retrieve an existing plan from the class by identifier, but return
	 * null if no such plan exists.
	 * 
	 * @param id
	 * @return
	 */
	public Plan getPlan(Predicate id) {
		List<Plan> list = planMap.get(id.identifier());
		if (list == null) return null;
		for (Plan plan : list) {
			if (plan.getLabel().matches(id)) {
				return plan;
			}
		}

		List<ASTRAClass> classes = getParentClasses();
		if (!classes.isEmpty()) {
			for (ASTRAClass clazz : classes) {
				Plan plan = clazz.getPlan(id);
				if (plan != null) return plan;
			}
		}
		
		return null;
	}


	public void addBelief(Predicate belief) {
		beliefs.add(belief);
	}
	
	public String toString() {
		String option = "agent " + name;
		if (parents != null) {
			option += " extends " + parents;
		}
		option += " {\n";
		for (List<Rule> list : rules.values()) {
			for (Rule rule : list) {
				option += rule.toFullString("\t") + "\n";
			}
		}
		
		return option + "}";
	}

	public void addFunction(TRFunction function) {
		functions.put(function.identifier().signature(), function);
		
	}

	public TRFunction getFunction(Predicate predicate) {
		return functions.get(predicate.signature()).copy();
	}

	public boolean handleEvent(Event event, Agent agent) {
		List<Rule> list = rules.get(event.signature());
		if (list == null) return false;
		
		for (Rule opt : list) {
			if (event.getClass().equals(opt.event().getClass())) {
				Rule option = opt.copy();
				Bindings b = Unifier.unify(event, option.event());

				if (b != null) {
					option = option.apply(b);
					try {
						List<Bindings> bindings = agent.query(option.context());
						if (bindings != null) {
							if (event.source() == null) {
								if (bindings.isEmpty()) {
									agent.intentions().add(new PlanStack(event.toString(), option.plan()));
								} else {
									agent.intentions().add(new PlanStack(event.toString(), option.plan().apply(bindings.get(0))));
								}
							} else {
								PlanStack stack = (PlanStack) event.source();
								if (bindings.isEmpty()) {
									stack.addPlanStep(option.plan());
								} else {
									stack.addPlanStep(option.plan().apply(bindings.get(0)));
								}
								stack.resume();
							}
							
							return true;
						}
					} catch (Throwable th) {
						System.out.println("Run-time Error: Could not handle rule:\n" + option.toString() + "\n" + th.getMessage());
						th.printStackTrace();
					}
				}
			}
		}
		
		
		if (event.source() != null) {
			((PlanStack) event.source()).fail();
		}
		return false;
	}

	public static Set<String> getInstanceNames() {
		return agents.keySet();
	}
	
	// Unit Testing Methods
	public synchronized void removeAgent(String name) {		
		agents.remove(name);
	}
	
	public boolean verifyEvent(Event event, Agent agent) {
		List<Rule> list = rules.get(event.signature());
		if (list == null) return false;
		
		for (Rule opt : list) {
			if (event.getClass().equals(opt.event().getClass())) {
				Rule option = opt.copy();
				Bindings b = Unifier.unify(event, option.event());

				if (b != null) {
					option = option.apply(b);
					try {
						List<Bindings> bindings = agent.query(option.context());
						if (bindings != null) {							
							return true;
						}
					} catch (Throwable th) {
						System.out.println("Run-time Error: Could not handle rule:\n" + option.toString() + "\n" + th.getMessage());
						th.printStackTrace();
					}
				}
			}
		}		
		
		if (event.source() != null) {
			((PlanStack) event.source()).fail();
		}
		return false;
	}

	public void addIntention(Predicate predicate, Agent agent) {
		agent.intentions().add(new PlanStack(predicate.toString(), new PlanInvocationStep(this, predicate)));
	}
	
	public boolean verifyRule(Rule rule) {
		List<Rule> list = rules.get(rule.event().signature());
		if (list == null) {
			return false;
		}
		
		for (Rule opt : list) {
			if (rule.event().getClass().equals(opt.event().getClass()) && opt.context().toString().equals(rule.context().toString())) {
				return true; 
			}
			
		}
		return false;
	}

	public boolean handleRule(Rule rule, Agent agent) {
		List<Rule> list = rules.get(rule.event().signature());
		if (list == null) {
			return false;
		}
		
		for (Rule opt : list) {
			if (rule.event().getClass().equals(opt.event().getClass()) && opt.context().toString().equals(rule.context().toString())) {
				Rule option = opt.copy();

				Bindings b = Unifier.unify(rule.event(), option.event());

				if (b != null) {
					option = option.apply(b);
					try {
						//List<Bindings> bindings = agent.query(option.context());
						//if (bindings != null) {
							if (rule.event().source() == null) {
//								if (bindings.isEmpty()) {
									agent.intentions().add(new PlanStack(rule.event().toString(), option.plan()));
//								} else {
//									agent.intentions().add(new PlanStack(rule.event().toString(), option.plan().apply(bindings.get(0))));
//								}
							} else {
								PlanStack stack = (PlanStack) rule.event().source();
//								if (bindings.isEmpty()) {
									stack.addPlanStep(option.plan());
//								} else {
//									stack.addPlanStep(option.plan().apply(bindings.get(0)));
//								}
								stack.resume();
							}
							
							return true;
//						}
					} catch (Throwable th) {
						System.out.println("Run-time Error: Could not handle rule:\n" + option.toString() + "\n" + th.getMessage());
						th.printStackTrace();
					}
				}
			}
		}
		
		
		if (rule.event().source() != null) {
			((PlanStack) rule.event().source()).fail();
		}
		return false;
	}
}
